/**
 * 
 */
package cz.cuni.mff.abacs.burglar.logics.objects.agents;

import cz.cuni.mff.abacs.burglar.logics.objects.BaseInterface;
import cz.cuni.mff.abacs.burglar.logics.objects.Room;
import cz.cuni.mff.abacs.burglar.logics.objects.items.Item;
import cz.cuni.mff.abacs.burglar.logics.objects.positions.Camera;
import cz.cuni.mff.abacs.burglar.logics.objects.positions.Container;
import cz.cuni.mff.abacs.burglar.logics.objects.positions.Door;
import cz.cuni.mff.abacs.burglar.logics.objects.positions.Position;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;


/**
 * Representation of the knowledge base of an agent.
 * 
 * @author abacs
 *
 */
public class BeliefBase {
	
	
	/** Remembered positions from a distance.
	 * Held by identifier.
	 * Changeable property. */
	protected Map<Integer, Position> _positionsFar = 
		new HashMap<Integer, Position>();
	
	
	/** Remembered positions after examination.
	 * Held by identifier.
	 * Changeable property. */
	protected Map<Integer, Position> _positionsNear = 
		new HashMap<Integer, Position>();
	
	
	/** IDs of the containers where the known items are.
	 * Position Ids by Item Id.
	 * Currently not in use.
	 * 
	 * Changeable property. */
	protected Map<Integer, Integer> _itemLocations = 
		new HashMap<Integer, Integer>();
	
	
	/**  */
	protected List<Integer> _roomIds = new LinkedList<Integer>();
	
	
	/**
	 * Remembered agents by their identifiers.
	 */
	protected Map<Integer, Agent> _agents = new HashMap<Integer, Agent>();
	
	
	// -------------------------------------------------------------------------
	
	
	/**
	 * Items are seen from a distance, some details are incorrect.
	 * 
	 * @return true if something new was added.
	 */
	public boolean seenFromFar(Position examined) {
		boolean seenSomethingNew = false;
		
		int id = examined.getId();
		
		// add the knowledge about the position if it did not exist so far:
		
		if(this._positionsFar.containsKey(id)){
			Position remembered = this._positionsFar.get(id);
			if(remembered.matchesFromFar(examined) == false){
				this._positionsFar.remove(id);
				
				Position ourCopy = examined.examinedFromFar();
				this._positionsFar.put(id, ourCopy);
				if(ourCopy.isTypeOf(BaseInterface.Type.CONTAINER))
					this.seenContents((Container)ourCopy);
				
				seenSomethingNew = true;
			}
			
			return seenSomethingNew;
		}
		
		if(this._positionsNear.containsKey(id)){
			Position remembered = this._positionsNear.get(id);
			if(remembered.matchesFromFar(examined) == false){
				Position oldPosition = this._positionsNear.remove(id);
				
				Position ourCopy = examined.examinedFromFar();
				this._positionsFar.put(id, ourCopy);
				if(ourCopy.isTypeOf(BaseInterface.Type.CONTAINER)){
					this.seenContents((Container)ourCopy);
					// don't forget the old items either:
					((Container)ourCopy).addItems(((Container)oldPosition).getItems());
				}
				
				seenSomethingNew = true;
			}
			
			return seenSomethingNew;
		}
		
		// no list contains it yet:
		
		Position ourCopy = examined.examinedFromFar();
		this._positionsFar.put(id, ourCopy);
		if(ourCopy.isTypeOf(BaseInterface.Type.CONTAINER))
			this.seenContents((Container)ourCopy);
		
		seenSomethingNew = true;
		
		// add parent room to room list:
		this.addContainingRoom(examined);
		
		return seenSomethingNew;
	}
	
	
	/**
	 * Items are seen from a distance, some details are incorrect.
	 * 
	 * @return true if something new was added.
	 */
	public boolean seenFromFar(List<Position> positions) {
		boolean seenSomethingNew = false;
		
		for(Position examined : positions){
			// add the knowledge about the examined position if it did not exist so far:
			seenSomethingNew = this.seenFromFar(examined) || seenSomethingNew;
		}
		
		return seenSomethingNew;
	}
	
	
	/**
	 * Item is seen from a small distance, all details are correct.
	 * 
	 * @return true if something new was added.
	 */
	public boolean seenFromNear(List<Position> positions) {
		boolean seenSomethingNew = false;
		
		for(Position pos : positions){
			// add the knowledge about the position if it did not exist so far:
			seenSomethingNew = this.seenFromNear(pos) || seenSomethingNew;
		}
		
		return seenSomethingNew;
	}
	
	
	/**
	 * Item is seen from a small distance, all details are correct.
	 * 
	 * @return true if something new was added.
	 */
	public boolean seenFromNear(Position examined) {
		boolean seenSomethingNew = false;
		int id = examined.getId();
		
		if(this._positionsFar.containsKey(id)){
			Position remembered = this._positionsFar.remove(id);
			if(remembered.matchesFromClose(examined)){
				this._positionsNear.put(id, remembered);
			}else{
				
				Position ourCopy = examined.examinedFromNear();
				this._positionsNear.put(id, ourCopy);
				if(ourCopy.isTypeOf(BaseInterface.Type.CONTAINER))
					this.seenContents((Container)ourCopy);
				
				seenSomethingNew = true;
			}
			
			return seenSomethingNew;
		}
		
		if(this._positionsNear.containsKey(id)){
			Position remembered = this._positionsNear.get(id);
			if(remembered.matchesFromClose(examined) == false){
				this._positionsNear.remove(id);
				
				Position ourCopy = examined.examinedFromNear();
				this._positionsNear.put(id, ourCopy);
				if(ourCopy.isTypeOf(BaseInterface.Type.CONTAINER))
					this.seenContents((Container)ourCopy);
				
				seenSomethingNew = true;
			}
			
			return seenSomethingNew;
		}
		
		Position ourCopy = examined.examinedFromNear();
		this._positionsNear.put(id, ourCopy);
		if(ourCopy.isTypeOf(BaseInterface.Type.CONTAINER))
			this.seenContents((Container)ourCopy);
		
		seenSomethingNew = true;
		
		// add parent room to room list:
		this.addContainingRoom(examined);
		
		return seenSomethingNew;
	}
	
	
	/**
	 * An agent observed from far.
	 * 
	 * @param observedAgent
	 * @return true if seen something new 
	 */
	public boolean seenFromFar(Agent observedAgent) {
		int agentId = observedAgent.getId();
		if(this.isKnownAgent(agentId)){
			Agent remembered = this._agents.get(agentId);
			if(remembered.matchesFromFar(observedAgent))
				return false;
		}
		this._agents.put(agentId, observedAgent.examinedFromFar());
		
		// add parent room to room list:
		this.addRoom(observedAgent.getRoomId());
		
		return true;
	}
	
	
	/**
	 * Examines the contents of a container.
	 * 
	 * @param container
	 * @return 
	 */
	private boolean seenContents(Container container) {
		boolean seenSomethingNew = false;
		
		for(Item item : container.getItems()){
			if(this._itemLocations.containsKey(item.getId())){
				if(this._itemLocations.get(item.getId()) != container.getId()){
					this._itemLocations.remove(item.getId());
					this._itemLocations.put(item.getId(), container.getId());
					seenSomethingNew = true;
				}
			}else{
				this._itemLocations.put(item.getId(), container.getId());
				seenSomethingNew = true;
			}
		}
		
		return seenSomethingNew;
	}
	
	
	/**
	 * 
	 * 
	 * @param roomId 
	 */
	private void addRoom(int roomId) {
		if(this._roomIds.contains(roomId))
			return;
		this._roomIds.add(roomId);
	}
	
	
	/**
	 * 
	 * @param position 
	 */
	private void addContainingRoom(Position position) {
		if(position.isTypeOf(BaseInterface.Type.DOOR)){
			int[] roomIds = ((Door)position).getRoomIds();
			this.addRoom(roomIds[0]);
			this.addRoom(roomIds[1]);
		}else{
			this.addRoom(position.getRoomId());
		}
	}
	
	
	/**
	 * Saves the location of an item.
	 */
	public void addItemPosition(Item item, Position position) {
		this._itemLocations.put(item.getId(), position.getId());
	}
	
	
	/**
	 * Removes the location of a given item.
	 */
	public void forgetItemPosition(int itemId) {
		this._itemLocations.remove(itemId);
	}
	
	
	/**
	 * Removes the location of a given item.
	 */
	public void forgetItemPosition(Item item) {
		this.forgetItemPosition(item.getId());
	}
	
	
	// -------------------------------------------------------------------------
	
	
	/** 
	 * Returns all the positions the agent knows about in their believed state.
	 * 
	 * It includes all items, seen near or far.
	 */
	public List<Position> getKnownPositions() {
		List<Position> ret = new LinkedList<Position>();
		ret.addAll(this._positionsFar.values());
		ret.addAll(this._positionsNear.values());
		return ret;
	}
	
	
	public Position getKnownPosition(int id) {
		if(this._positionsFar.containsKey(id))
			return this._positionsFar.get(id);
		
		return this._positionsNear.get(id);
	}
	
	
	/**
	 * Returns known stunned agents.
	 */
	public List<Agent> getPassiveAgents() {
		List<Agent> ret = new LinkedList<Agent>();
		for(Agent agent : this._agents.values()){
			if(agent.isInState(Agent.State.STUNNED)){
				ret.add(agent);
			}
		}
		return ret;
	}
	
	
	/** 
	 * Returns the identifier of the item holder.
	 * 
	 * Returns null if nothing found.
	 */
	public Integer getItemPositionId(int itemId) {
		if(this._itemLocations.containsKey(itemId)){
			return this._itemLocations.get(itemId);
		}
		return null;
	}
	
	
	/** 
	 * Returns the identifier of the item holder.
	 * 
	 * Returns null if nothing found.
	 */
	public Integer getItemPositionId(Item item) {
		return this.getItemPositionId(item.getId());
	}
	
	
	/**
	 * Decides whether the agent knows the position.
	 */
	public boolean isKnownPosition(int positionId) {
		return
			this._positionsFar.containsKey(positionId) ||
			this._positionsNear.containsKey(positionId);
	}
	
	
	/**
	 * Decides whether the agent knows the position.
	 */
	public boolean isKnownPosition(Position position) {
		if(position == null)
			return false;
		else
			return this.isKnownPosition(position.getId());
	}
	
	
	/**
	 * Decides whether the agent knows the other agent.
	 */
	public boolean isKnownAgent(int agentId) {
		return this._agents.containsKey(agentId);
	}
	
	
	/**
	 * Decides whether the agent knows the other agent.
	 */
	public boolean isKnownAgent(Agent agent) {
		if(agent == null)
			return false;
		else
			return this.isKnownAgent(agent.getId());
	}
	
	
	/**
	 * Decides whether the agent knows the item.
	 */
	public boolean isKnownItemLocation(int itemId) {
		return this._itemLocations.containsKey(itemId);
	}
	
	
	/**
	 * Decides whether the agent knows the item.
	 */
	public boolean isKnownItemLocation(Item item) {
		return this.isKnownItemLocation(item.getId());
	}
	
	
	/**
	 * 
	 * @param roomId
	 * @return 
	 */
	public boolean isKnownRoom(int roomId) {
		return this._roomIds.contains(roomId);
	}
	
	
	/**
	 * 
	 * @param room
	 * @return 
	 */
	public boolean isKnownRoom(Room room) {
		return this._roomIds.contains(room.getId());
	}
	
	
	/**
	 * 
	 * @param roomId
	 * @return 
	 */
	public boolean isObservedRoom(int roomId) {
		for(Position position : this._positionsFar.values()){
			if(
				position.isTypeOf(BaseInterface.Type.CAMERA) &&
				((Camera)position).isActive() &&
				position.getRoomId() == roomId
			)
				return true;
		}
		
		for(Position position : this._positionsNear.values()){
			if(
				position.isTypeOf(BaseInterface.Type.CAMERA) &&
				((Camera)position).isActive() &&
				position.getRoomId() == roomId
			)
				return true;
		}
		
		for(Agent agent : this._agents.values()){
			if(
				agent.isTypeOf(BaseInterface.Type.GUARD) &&
				((Guard)agent).isInState(Agent.State.WELL) &&
				agent.getRoomId() == roomId
			)
				return true;
		}
		
		return false;
	}
	
	
	/**
	 * 
	 * @param room
	 * @return 
	 */
	public boolean isObservedRoom(Room room) {
		return this.isObservedRoom(room.getId());
	}
	
	
	/**
	 * Returns whether the position matches with something in the belief base.
	 */
	public boolean matches(Position examined) {
		Position own = this._positionsFar.get(examined.getId());
		if(own != null)
			return own.matchesFromClose(examined);
		
		own = this._positionsNear.get(examined.getId());
		if(own != null)
			return own.matchesFromClose(examined);
		
		return false;
	}
	
	
	/**
	 * Returns whether the agent matches with something in the belief base.
	 */
	public boolean matches(Agent examined) {
		Agent own = this._agents.get(examined.getId());
		if(own != null)
			return own.matchesFromFar(examined);
		else
			return false;
	}
	
	
	/**
	 * Copies the beliefs of another belief base.
	 * 
	 * Does not change the map!
	 * 
	 * @param original
	 */
	public void copyChangeables(BeliefBase original) {
		
		for(Position pos : original._positionsFar.values())
			this._positionsFar.put(pos.getId(), pos);
		
		for(Position pos : original._positionsNear.values())
			this._positionsNear.put(pos.getId(), pos);
		
		for(Entry<Integer, Integer> pair : original._itemLocations.entrySet())
			this._itemLocations.put(pair.getKey(), pair.getValue());
		
		for(Integer roomId : original._roomIds)
			this._roomIds.add(roomId);
	}
	
}
